Conversation
Reviewer's GuideInitial Bubble365 Deskpro app scaffold using the standard Deskpro React/Vite template, wiring a Bubble365 iframe integration with postMessage-based communication, Sentry error reporting, React Query, and setting up CI/CD workflows, testing, packaging, and manifest for deployment to the Deskpro app marketplace. Sequence diagram for Bubble365 iframe call start requestsequenceDiagram
actor Agent
participant DeskproApp as DeskproApp_Main
participant ParentService as ParentIframeService
participant BubbleIframe as Bubble365_iframe
Agent->>DeskproApp: Click startCall (future use)
DeskproApp->>ParentService: sendRequest(call, start, {number})
activate ParentService
ParentService->>ParentService: createMessage(call, start, payload, null)
ParentService->>BubbleIframe: postMessage(JSON.stringify(request), "*")
deactivate ParentService
Note over BubbleIframe: Bubble app handles call start
BubbleIframe-->>ParentService: postMessage(JSON.stringify(response))
activate ParentService
ParentService->>ParentService: onMessage(event)
ParentService->>ParentService: JSON.parse(event.data)
ParentService->>ParentService: Match response_id in pending
ParentService-->>DeskproApp: Resolve Promise<Message<{success, error?}>>
deactivate ParentService
alt success
DeskproApp->>DeskproApp: console.log("Call started")
else failure
DeskproApp->>DeskproApp: console.error("Call failed", error)
end
Sequence diagram for Bubble365 iframe initialization and UI controlsequenceDiagram
participant ParentService as ParentIframeService
participant BubbleIframe as Bubble365_iframe
participant DeskproClient as DeskproAppClient
Note over ParentService,BubbleIframe: After iframe loads, ParentIframeService is created
BubbleIframe-->>ParentService: postMessage("{protocol:bubble365, channel:initialize, cmd:request}")
activate ParentService
ParentService->>ParentService: onMessage(event)
ParentService->>BubbleIframe: sendMessage(initialize, request, {success:true, settings})
ParentService->>DeskproClient: handlers.onInitialized()
deactivate ParentService
BubbleIframe-->>ParentService: postMessage("{channel:badge, cmd:set, payload:{count}}")
activate ParentService
ParentService->>DeskproClient: handlers.onBadgeSet(count)
deactivate ParentService
BubbleIframe-->>ParentService: postMessage("{channel:widget, cmd:set, payload:{visible}}")
activate ParentService
alt visible is true
ParentService->>DeskproClient: handlers.onWidgetSet(true) -> focus()
else visible is false or undefined
ParentService->>DeskproClient: handlers.onWidgetSet(false) -> unfocus()
end
deactivate ParentService
BubbleIframe-->>ParentService: postMessage("{channel:hyperlink, cmd:open, payload:{url,target}}");
activate ParentService
ParentService->>ParentService: handleHyperlinkOpenRequest()
alt url is Deskpro app URL
ParentService->>ParentService: window.parent.location.replace(url)
else external URL
ParentService->>ParentService: window.open(url, target or _blank)
end
deactivate ParentService
Class diagram for Bubble365 iframe integration and app bootstrapclassDiagram
class ParentIframeService {
- HTMLIFrameElement iframe
- Map~string, function~ pending
- ParentHandlers handlers
- function boundOnMessage
- Settings settings
+ ParentIframeService(iframe: HTMLIFrameElement, handlers: ParentHandlers, settings: Settings)
+ sendMessage(TPayload)(channel: PostMessageChannel, cmd: string, payload: TPayload, response_id: string) void
+ sendRequest(TRequest, TResponse)(channel: PostMessageChannel, cmd: string, payload: TRequest, timeoutMs: number) Promise~Message~TResponse~~
+ destroy() void
- createMessage(TPayload)(channel: PostMessageChannel, cmd: string, payload: TPayload, responseId: string) Message~TPayload~
- onMessage(event: MessageEvent) void
- handleHyperlinkOpenRequest(request: Message~unknown~) void
}
class Settings {
+ override_hyperlink_open: boolean
}
class BadgeSetPayload {
+ count: number
}
class FocusPayload {
+ visible: boolean
}
class HyperlinkOpenPayload {
+ url: string
+ target: string
+ button: unknown
}
class PostMessageChannel {
}
class ParentHandlers {
+ onInitialized(): void
+ onBadgeSet(count: number): void
+ onWidgetSet(visible: boolean): void
+ onHyperlinkOpen(url: string, target: string, button: unknown): void
}
class Message~TPayload~ {
+ protocol: string
+ version: number
+ id: string
+ response_id: string
+ channel: PostMessageChannel
+ cmd: string
+ payload: TPayload
}
class Main {
+ clientRef: MutableRefObject
+ iframeRef: MutableRefObject
+ iframeServiceRef: MutableRefObject
+ useEffect() void
+ startCall() Promise~void~
}
class App {
+ App()
}
class queryClient {
+ QueryClient
}
class DeskproAppProvider {
}
class HashRouter {
}
class QueryClientProvider {
}
class ErrorBoundary {
}
class LoadingSpinner {
}
class SentryInit {
+ init()
}
ParentIframeService --> ParentHandlers : uses
ParentIframeService --> Message : creates
Message --> PostMessageChannel : uses
ParentIframeService --> Settings : has
ParentIframeService --> BadgeSetPayload : uses
ParentIframeService --> FocusPayload : uses
ParentIframeService --> HyperlinkOpenPayload : uses
Main --> ParentIframeService : creates and stores
Main --> queryClient : uses
Main --> DeskproAppProvider : rendered within
Main --> HashRouter : rendered within
Main --> QueryClientProvider : rendered within
App --> Main : routes to
SentryInit --> ErrorBoundary : wraps React tree
QueryClientProvider --> queryClient : provides
class instrument_ts {
+ configureSentry()
}
instrument_ts --> SentryInit : calls
class index_html {
+ root_div
}
App --> index_html : rendered into root_div
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 11 issues, and left some high level feedback:
- The postMessage bridge in ParentIframeService currently uses a wildcard targetOrigin ('*') and does not check event.origin; consider tightening this by deriving the expected origin from VITE_BUBBLE_URL and validating event.origin before processing messages to reduce the risk of handling messages from unexpected sources.
- ParentIframeService.destroy() silently deletes all pending requests without notifying callers; it may be helpful to reject those pending Promises explicitly (e.g. by storing reject callbacks) so consumers can handle teardown-related failures instead of hanging indefinitely.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The postMessage bridge in ParentIframeService currently uses a wildcard targetOrigin ('*') and does not check event.origin; consider tightening this by deriving the expected origin from VITE_BUBBLE_URL and validating event.origin before processing messages to reduce the risk of handling messages from unexpected sources.
- ParentIframeService.destroy() silently deletes all pending requests without notifying callers; it may be helpful to reject those pending Promises explicitly (e.g. by storing reject callbacks) so consumers can handle teardown-related failures instead of hanging indefinitely.
## Individual Comments
### Comment 1
<location> `src/types/services/parentIframeService.ts:84-93` </location>
<code_context>
+ }
+ }
+
+ private onMessage(event: MessageEvent): void {
+ if (typeof event.data !== "string") return
+
+ console.debug("[ParentIframeService] Received message", event.data)
+
+ let parsed: unknown
+ try {
+ parsed = JSON.parse(event.data)
+ } catch {
+ return
+ }
+
+ const msg = parsed as Message<unknown>
+ if (!msg || msg.protocol !== "bubble365" || msg.version !== 1) return
+
</code_context>
<issue_to_address>
**🚨 issue (security):** Consider validating the message origin/source before trusting event.data.
`onMessage` only validates the payload structure and not `event.origin` or `event.source`. Another frame on the page could spoof `bubble365` messages. Please also verify `event.origin` (and/or `event.source === this.iframe.contentWindow`) matches the expected iframe before parsing/handling the message.
</issue_to_address>
### Comment 2
<location> `src/types/services/parentIframeService.ts:31-32` </location>
<code_context>
+
+ public sendMessage<TPayload>(channel: PostMessageChannel, cmd: string, payload?: TPayload, response_id?: string | null): void {
+ const message = this.createMessage(channel, cmd, payload, response_id)
+ console.debug("[ParentIframeService] Post message", message)
+ this.iframe.contentWindow?.postMessage(JSON.stringify(message), "*")
+ }
+
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Using `postMessage` with `"*"` targetOrigin is permissive; consider tightening it.
Both `sendMessage` and `sendRequest` currently call `postMessage(..., "*")`. If the Bubble iframe’s origin is known, pass that explicitly instead to avoid messages being deliverable to unintended windows if the frame hierarchy changes or is reused.
Suggested implementation:
```typescript
export class ParentIframeService {
private iframe: HTMLIFrameElement
private pending = new Map<string, (msg: Message<unknown>) => void>()
private targetOrigin: string
```
```typescript
this.settings = settings
console.debug("[ParentIframeService] starting service..")
try {
// Derive the Bubble iframe origin from its src (supports absolute and relative URLs)
this.targetOrigin = new URL(this.iframe.src, window.location.href).origin
} catch {
// Fallback to permissive origin if parsing fails; consider tightening further if possible
console.warn("[ParentIframeService] Unable to determine iframe origin, falling back to '*'.")
this.targetOrigin = "*"
}
this.boundOnMessage = this.onMessage.bind(this)
window.addEventListener("message", this.boundOnMessage)
```
```typescript
public sendMessage<TPayload>(channel: PostMessageChannel, cmd: string, payload?: TPayload, response_id?: string | null): void {
const message = this.createMessage(channel, cmd, payload, response_id)
console.debug("[ParentIframeService] Post message", message)
this.iframe.contentWindow?.postMessage(JSON.stringify(message), this.targetOrigin)
```
```typescript
this.iframe.contentWindow?.postMessage(JSON.stringify(message), this.targetOrigin)
```
There is also a `sendRequest` method in this class that currently calls `postMessage(..., "*")` but is not shown in your snippet. You should update that call in the same way:
- Replace `this.iframe.contentWindow?.postMessage(JSON.stringify(message), "*")` with `this.iframe.contentWindow?.postMessage(JSON.stringify(message), this.targetOrigin)` inside `sendRequest`.
If there are any other direct `postMessage` calls targeting `"*"` in this file, they should be updated to use `this.targetOrigin` as well.
</issue_to_address>
### Comment 3
<location> `src/pages/Main/Main.tsx:7` </location>
<code_context>
+} from "@deskpro/app-sdk"
+import {ParentIframeService} from "@/types/services/parentIframeService"
+
+const BUBBLE_URL = import.meta.env.VITE_BUBBLE_URL as string
+
+export const Main = () => {
</code_context>
<issue_to_address>
**suggestion:** Add a runtime guard or sensible fallback if `VITE_BUBBLE_URL` is missing.
The `as string` cast assumes `VITE_BUBBLE_URL` is always defined. If it’s missing, the iframe ends up with `src="undefined"`, which is misleading. Add a runtime check (e.g., at startup) and either log a clear error, show a fallback UI, or skip rendering the iframe when the env var is absent.
Suggested implementation:
```typescript
const BUBBLE_URL = import.meta.env.VITE_BUBBLE_URL
export const Main = () => {
const {client} = useDeskproAppClient()
const clientRef = useRef<typeof client | null>(null)
const iframeRef = useRef<HTMLIFrameElement | null>(null)
const iframeServiceRef = useRef<ParentIframeService | null>(null)
if (!BUBBLE_URL) {
// Fail fast with a clear runtime signal instead of rendering a broken iframe.
console.error(
"[Deskpro Bubble App] Environment variable VITE_BUBBLE_URL is not defined. " +
"The Bubble iframe cannot be rendered. Please set VITE_BUBBLE_URL in your environment."
)
// You can replace this with a more tailored fallback component if desired.
return (
<div>
Configuration error: Bubble URL is not configured. Please contact an administrator.
</div>
)
}
```
If other parts of this file (or other modules) rely on `BUBBLE_URL` being typed as `string`, you may need to:
1. Update those usages to either:
- Use a locally narrowed variable (e.g., `const bubbleUrl = BUBBLE_URL as string` *after* the guard), or
- Perform similar runtime guards before using `BUBBLE_URL`.
2. Ensure that the iframe `src` in this component is only ever set to a non-empty string (e.g., use `BUBBLE_URL` after this guard or pass a narrowed `bubbleUrl`).
</issue_to_address>
### Comment 4
<location> `src/pages/Main/Main.tsx:33-38` </location>
<code_context>
+ onBadgeSet: (count) => {
+ void clientRef.current?.setBadgeCount?.(count)
+ },
+ onWidgetSet: (visible) => {
+ if (visible)
+ clientRef.current?.focus()
+ else
+ clientRef.current?.unfocus()
+ },
+ onHyperlinkOpen: (url, target) => {
</code_context>
<issue_to_address>
**suggestion:** Clarify handling of undefined `visible` to avoid unintended unfocus.
Because `undefined` is treated as falsy, omitting `visible` will currently trigger `unfocus()`. If the protocol allows `visible` to be omitted, consider unfocusing only when `visible === false`, or focusing only when `visible === true` and ignoring `undefined`.
```suggestion
onWidgetSet: (visible) => {
if (visible === true) {
clientRef.current?.focus()
} else if (visible === false) {
clientRef.current?.unfocus()
}
},
```
</issue_to_address>
### Comment 5
<location> `README.md:22` </location>
<code_context>
+With the Bubble365 Embedded CRM App you experience the power of the Bubble integration tool, fully integrated within your CRM environment.
+The Bubble365 Embedded CRM App is compatible with 90+ telephony platforms.
+If your telephony platform supports Call Control, you manage your softphone or connected desk phone directly from the CRM - with features such as take calls, transfer calls, put on hold and hang up.
</code_context>
<issue_to_address>
**suggestion (typo):** Add a comma after "App" for correct sentence structure.
Consider revising the sentence to: "With the Bubble365 Embedded CRM App, you experience the power of the Bubble integration tool, fully integrated within your CRM environment."
```suggestion
With the Bubble365 Embedded CRM App, you experience the power of the Bubble integration tool, fully integrated within your CRM environment.
```
</issue_to_address>
### Comment 6
<location> `README.md:27` </location>
<code_context>
+If your telephony platform supports Call Control, you manage your softphone or connected desk phone directly from the CRM - with features such as take calls, transfer calls, put on hold and hang up.
+The app provides a seamless user experience and increases productivity by bringing all communication and CRM functionalities together in one interface.
+
+**Pop-up Notification**: Instantly see all customer information on an incoming, outgoing and transferred call.
+
+**SearchBar**: Search directly in Deskpro contacts, open a customer card or send a message.
</code_context>
<issue_to_address>
**suggestion (typo):** Use plural "calls" to agree with the list of call types.
Given the list format, I suggest ending the sentence with: "on incoming, outgoing, and transferred calls."
```suggestion
**Pop-up Notification**: Instantly see all customer information on incoming, outgoing, and transferred calls.
```
</issue_to_address>
### Comment 7
<location> `README.md:40-41` </location>
<code_context>
+You can follow our [wiki guide](https://wiki.redcactus.cloud/en/crm-software/Deskpro-embedded) for a step-by-step guide to setting up the app in Deskpro.
+
+## Development
+This app was developed primarily using **Typescript**, **React**, and **Vite**.
+
+#### Setup
</code_context>
<issue_to_address>
**issue (typo):** Correct the capitalization of "TypeScript".
The official spelling is "TypeScript", so this sentence should read "**TypeScript**, **React**, and **Vite**."
```suggestion
## Development
This app was developed primarily using **TypeScript**, **React**, and **Vite**.
```
</issue_to_address>
### Comment 8
<location> `DESCRIPTION.md:1` </location>
<code_context>
+With the Bubble365 Embedded CRM App you experience the power of the Bubble integration tool, fully integrated within your CRM environment.
+The Bubble365 Embedded CRM App is compatible with 90+ telephony platforms.
+If your telephony platform supports Call Control, you manage your softphone or connected desk phone directly from the CRM - with features such as take calls, transfer calls, put on hold and hang up.
</code_context>
<issue_to_address>
**suggestion (typo):** Add a comma after "App" for grammatical correctness.
For example: "With the Bubble365 Embedded CRM App, you experience the power of the Bubble integration tool, fully integrated within your CRM environment."
```suggestion
With the Bubble365 Embedded CRM App, you experience the power of the Bubble integration tool, fully integrated within your CRM environment.
```
</issue_to_address>
### Comment 9
<location> `DESCRIPTION.md:3` </location>
<code_context>
+With the Bubble365 Embedded CRM App you experience the power of the Bubble integration tool, fully integrated within your CRM environment.
+The Bubble365 Embedded CRM App is compatible with 90+ telephony platforms.
+If your telephony platform supports Call Control, you manage your softphone or connected desk phone directly from the CRM - with features such as take calls, transfer calls, put on hold and hang up.
+The app provides a seamless user experience and increases productivity by bringing all communication and CRM functionalities together in one interface.
+
</code_context>
<issue_to_address>
**suggestion (typo):** Improve the sentence flow ("you can manage" and gerunds for call actions).
For example: “If your telephony platform supports Call Control, you can manage your softphone or connected desk phone directly from the CRM, with features such as taking calls, transferring calls, putting calls on hold, and hanging up.”
```suggestion
If your telephony platform supports Call Control, you can manage your softphone or connected desk phone directly from the CRM, with features such as taking calls, transferring calls, putting calls on hold, and hanging up.
```
</issue_to_address>
### Comment 10
<location> `DESCRIPTION.md:6` </location>
<code_context>
+If your telephony platform supports Call Control, you manage your softphone or connected desk phone directly from the CRM - with features such as take calls, transfer calls, put on hold and hang up.
+The app provides a seamless user experience and increases productivity by bringing all communication and CRM functionalities together in one interface.
+
+**Pop-up Notification**: Instantly see all customer information on an incoming, outgoing and transferred call.
+
+**SearchBar**: Search directly in Deskpro contacts, open a customer card or send a message.
</code_context>
<issue_to_address>
**suggestion (typo):** Use plural "calls" to match the multiple call types.
To align with the list of call types, consider “…on incoming, outgoing, and transferred calls.”
```suggestion
**Pop-up Notification**: Instantly see all customer information on incoming, outgoing, and transferred calls.
```
</issue_to_address>
### Comment 11
<location> `SETUP.md:5` </location>
<code_context>
+===
+To use the Bubble app, you need a license for Bubble.
+
+When installed, you can log in with your Bubble credentials into the app in the topbar.
</code_context>
<issue_to_address>
**suggestion (typo):** Slightly rephrase for smoother grammar ("log in to the app" and possibly "top bar").
Suggestion: "When installed, you can log in to the app with your Bubble credentials from the top bar."
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
djgadd
left a comment
There was a problem hiding this comment.
A couple minor suggestions, take or leave them, I'm not going to block the on them 👍 Thanks for the PR!
Co-authored-by: Daniel <danjgadd@gmail.com>
I've addressed all the suggested changes, Thanks for the review! |
Initial code for the Bubble365 app. We just copied the template. Main thing we changed is the Main.tsx
Summary by Sourcery
Initialize the Bubble365 Deskpro app project with a React/Vite-based Deskpro app shell, CI/CD workflows, and packaging/versioning infrastructure.
New Features:
Enhancements:
Build:
Documentation: